﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using SkinEngines.Win32;
using System.Drawing;
using System.ComponentModel;
using System.Security.Permissions;
using System.Runtime.InteropServices;
using SkinEngines.SkinForm.Painting;
using System.Collections;
using System.Diagnostics;

namespace SkinEngines.SkinForm
{
    /// <summary>
    /// SkinningForm Inhirt NativeWindow to processes the window messages of a given Form to enable the skinning.
    /// </summary>
    public class SkinningDialog : NativeWindow
    {
        #region Fields
        /// <summary>
        /// parent form
        /// </summary>
        private IntPtr _parentHandler;

        /// <summary>
        /// caption Buttons
        /// </summary>
        private List<CaptionButton> _captionButtons;

        /// <summary>
        /// form is active
        /// </summary>
        private bool _formIsActive;

        /// <summary>
        /// Buffered Graphics Context
        /// </summary>
        private readonly BufferedGraphicsContext _bufferContext;

        /// <summary>
        /// buffer Graphics
        /// </summary>
        private BufferedGraphics _bufferGraphics;

        /// <summary>
        /// current Cache Size
        /// </summary>
        private Size _currentCacheSize;

        /// <summary>
        /// used for state resetting pressedButton
        /// </summary>
        private CaptionButton _pressedButton;

        /// <summary>
        /// used for state resetting hoveredButton
        /// </summary>
        private CaptionButton _hoveredButton;

        /// <summary>
        /// Skin Engine
        /// </summary>
        private SkinEngine _engine;

        /// <summary>
        /// skinControls Get List Control
        /// </summary>
        private Hashtable _skinControls = new Hashtable();
        #endregion

        #region Constructor and Destructor
        /// <summary>
        /// Initializes a new instance of the <see cref="SkinningForm"/> class.
        /// </summary>
        /// <param name="parentForm">The parent form.</param>
        /// <param name="engine">The engine.</param>
        public SkinningDialog(IntPtr handler, SkinEngine engine)
        {
            this._engine = engine;
            this._parentHandler = handler;
            this._captionButtons = new List<CaptionButton>();

            this._bufferContext = BufferedGraphicsManager.Current;
            this._bufferGraphics = null;

            this.SkinControls();

            if (handler != IntPtr.Zero)
                OnHandleCreated(handler, EventArgs.Empty);

            RegisterEventHandlers();
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="SkinningForm"/> is reclaimed by garbage collection.
        /// </summary>
        ~SkinningDialog()
        {
            UnregisterEventHandlers();
            OnHandleDestroyed(this, EventArgs.Empty);
        }
        #endregion

        #region SkinControls
        /// <summary>
        /// Skin All Controls
        /// </summary>
        protected void SkinControls()
        {
            // Get topWindow
            IntPtr topWindow = NativeMethod.GetTopWindow(this._parentHandler);
            ulong windowLong = 0L;
            // Get all Window 
            while (topWindow != IntPtr.Zero)
            {
                // Check Contains topWindow
                if (this._skinControls.ContainsKey(topWindow))
                {
                    topWindow = NativeMethod.GetWindow(topWindow, 2);
                    continue;
                }
                // get ClassName
                StringBuilder lpClassName = new StringBuilder(260);
                windowLong = (ulong)NativeMethod.GetWindowLong(topWindow, GWLIndex.GWL_STYLE);
                NativeMethod.GetClassName(topWindow, lpClassName, 260);

                Debug.WriteLine(lpClassName.ToString());


                topWindow = NativeMethod.GetWindow(topWindow, 2);
            }

            //foreach (Control control in this._parentForm.Controls)
            //{
            //    if (!this._skinControls.ContainsKey(control))
            //    {
            //        SkinControl.SkinControls skincontrol = ControlExtenders.GetSkinControl(control, this._engine);
            //        this._skinControls.Add(control, skincontrol);
            //    }
            //}
        }
        #endregion

        #region Parent Form Handlers
        /// <summary>
        /// Registers all important eventhandlers.
        /// </summary>
        private void RegisterEventHandlers()
        {
            //_parentForm.HandleCreated += OnHandleCreated;
            //_parentForm.HandleDestroyed += OnHandleDestroyed;
            //_parentForm.TextChanged += OnTextChanged;
            //_parentForm.Disposed += OnParentDisposed;
        }

        /// <summary>
        /// Unregisters all important eventhandlers.
        /// </summary>
        private void UnregisterEventHandlers()
        {
            //_parentForm.HandleCreated -= OnHandleCreated;
            //_parentForm.HandleDestroyed -= OnHandleDestroyed;
            //_parentForm.TextChanged -= OnTextChanged;
            //_parentForm.Disposed -= OnParentDisposed;
        }

        /// <summary>
        /// Called when the handle of the parent form is created.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnHandleCreated(object sender, EventArgs e)
        {
            // this little line allows us to handle the windowMessages of the parent form in this class
            AssignHandle(this._parentHandler);
            UpdateStyle();
            UpdateCaption();
        }

        /// <summary>
        /// Called when the handle of the parent form is destroyed
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnHandleDestroyed(object sender, EventArgs e)
        {
            // release handle as it is destroyed
            ReleaseHandle();
        }

        /// <summary>
        /// Called when the parent of the parent form is disposed
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnParentDisposed(object sender, EventArgs e)
        {
            // unregister events as the parent of the form is disposed
            if (_parentHandler != null)
                UnregisterEventHandlers();
            _parentHandler = IntPtr.Zero;
        }

        /// <summary>
        /// Called when the text on the parent form has changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnTextChanged(object sender, EventArgs e)
        {
            // Redraw on title change
            NcPaint(true);
        }
        #endregion

        #region Skinning Executors
        /// <summary>
        /// override WndProc to Skinning Form
        /// </summary>
        /// <param name="m"></param>
        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        protected override void WndProc(ref Message m)
        {
            bool supressOriginalMessage = false;

            switch ((Win32Messages)m.Msg)
            {
                #region Handle Form Activation
                case Win32Messages.ACTIVATEAPP:
                    // redraw
                    _formIsActive = (int)m.WParam != 0;
                    NcPaint(true);
                    break;

                case Win32Messages.ACTIVATE:
                    // Set active state and redraw
                    _formIsActive = ((int)WAFlags.WA_ACTIVE == (int)m.WParam || (int)WAFlags.WA_CLICKACTIVE == (int)m.WParam);
                    NcPaint(true);
                    break;
                #endregion

                #region Handle Mouse Processing
                // Set Pressed button on mousedown
                case Win32Messages.NCLBUTTONDOWN:
                    supressOriginalMessage = OnNcLButtonDown(ref m);
                    break;
                // Set hovered button on mousemove
                case Win32Messages.NCMOUSEMOVE:
                    OnNcMouseMove(m);
                    break;
                // perform button actions if a button was clicked
                case Win32Messages.NCLBUTTONUP:
                    // Handle button up
                    if (OnNcLButtonUp(m))
                        supressOriginalMessage = true;
                    break;
                // restore button states on mouseleave
                case Win32Messages.NCMOUSELEAVE:
                case Win32Messages.MOUSELEAVE:
                case Win32Messages.MOUSEHOVER:
                    if (_pressedButton != null)
                        _pressedButton.Pressed = false;
                    if (_hoveredButton != null)
                    {
                        _hoveredButton.Hovered = false;
                        _hoveredButton = null;
                    }
                    NcPaint(true);
                    break;
                #endregion

                #region Size Processing
                case Win32Messages.PAINT:
                    this._engine.CurrentSkin.OnPaint(_parentHandler);
                    break;
                // Set region as window is shown                    
                case Win32Messages.SHOWWINDOW:
                    // _engine.CurrentSkin.OnSetRegion(_parentHandler, _parentHandler.Size);
                    break;
                // update region on resize
                case Win32Messages.WINDOWPOSCHANGING:
                    WINDOWPOS wndPos = (WINDOWPOS)m.GetLParam(typeof(WINDOWPOS));
                    if ((wndPos.flags & (int)SWPFlags.SWP_NOSIZE) == 0)
                    {
                        // _engine.CurrentSkin.OnSetRegion(_parentHandler, new Size(wndPos.cx, wndPos.cy));
                    }
                    break;
                // remove region on maximize or repaint on resize
                case Win32Messages.WINDOWPOSCHANGED:
                    WINDOWPOS wndPos2 = (WINDOWPOS)m.GetLParam(typeof(WINDOWPOS));
                    if ((wndPos2.flags & (int)SWPFlags.SWP_NOSIZE) == 0)
                    {
                        UpdateCaption();
                        NcPaint(true);
                    }
                    break;
                #endregion

                #region Non Client Area Handling
                // paint the non client area
                case Win32Messages.NCPAINT:
                    if (NcPaint(true))
                    {
                        m.Result = (IntPtr)1;
                        supressOriginalMessage = true;
                    }
                    break;
                // calculate the non client area size
                case Win32Messages.NCCALCSIZE:
                    if (m.WParam == (IntPtr)1)
                    {
                        // add caption height to non client area
                        NCCALCSIZE_PARAMS p = (NCCALCSIZE_PARAMS)m.GetLParam(typeof(NCCALCSIZE_PARAMS));
                        p.rect0.Top += DialogExtenders.GetCaptionHeight();
                        Marshal.StructureToPtr(p, m.LParam, true);
                    }
                    break;
                // non client hit test
                case Win32Messages.NCHITTEST:
                    if (NcHitTest(ref m))
                        supressOriginalMessage = true;
                    break;
                #endregion
            }

            if (!supressOriginalMessage)
                base.WndProc(ref m);
        }

        /// <summary>
        /// Handles the mouse move event on the non client area.
        /// </summary>
        /// <param name="m">The m.</param>
        private void OnNcMouseMove(Message m)
        {
            // Check for hovered and pressed buttons
            if (Control.MouseButtons != MouseButtons.Left)
            {
                if (_pressedButton != null)
                {
                    _pressedButton.Pressed = false;
                    _pressedButton = null;
                }
            }
            CaptionButton button = CommandButtonByHitTest((HitTest)m.WParam);

            if (_hoveredButton != button && _hoveredButton != null)
                _hoveredButton.Hovered = false;
            if (_pressedButton == null)
            {
                if (button != null)
                    button.Hovered = true;
                _hoveredButton = button;
            }
            else
                _pressedButton.Pressed = (button == _pressedButton);
        }

        /// <summary>
        /// Handle a left mouse button down event.
        /// </summary>
        /// <param name="m">The m.</param>
        /// <returns></returns>
        private bool OnNcLButtonDown(ref Message m)
        {
            CaptionButton button = CommandButtonByHitTest((HitTest)m.WParam);
            if (_pressedButton != button && _pressedButton != null)
                _pressedButton.Pressed = false;
            if (button != null)
                button.Pressed = true;
            _pressedButton = button;
            if (_pressedButton != null)
            {
                m.Result = (IntPtr)1;
                return true;
            }
            return false;
        }

        /// <summary>
        /// Updates the window style for the parent form.
        /// </summary>
        private void UpdateStyle()
        {
            // remove the border style
            Int32 currentStyle = NativeMethod.GetWindowLong(this._parentHandler, GWLIndex.GWL_STYLE);
            if ((currentStyle & (int)(WindowStyles.WS_BORDER)) != 0)
            {
                currentStyle &= ~(int)(WindowStyles.WS_BORDER);
                NativeMethod.SetWindowLong(_parentHandler, GWLIndex.GWL_STYLE, currentStyle);
                NativeMethod.SetWindowPos(_parentHandler, (IntPtr)0, -1, -1, -1, -1,
                                      (int)(SWPFlags.SWP_NOZORDER | SWPFlags.SWP_NOSIZE | SWPFlags.SWP_NOMOVE |
                                             SWPFlags.SWP_FRAMECHANGED | SWPFlags.SWP_NOREDRAW | SWPFlags.SWP_NOACTIVATE));
            }
        }

        /// <summary>
        /// Updates the caption.
        /// </summary>
        private void UpdateCaption()
        {
            // create buttons
            if (_captionButtons.Count == 0)
            {
                _captionButtons.Add(new CaptionButton(HitTest.HTCLOSE));

                // add command handlers
                foreach (CaptionButton button in _captionButtons)
                    button.PropertyChanged += OnCommandButtonPropertyChanged;
            }

            // Calculate Caption Button Bounds
            RECT rectScreen = new RECT();
            NativeMethod.GetWindowRect(_parentHandler, ref rectScreen);
            Rectangle rect = rectScreen.ToRectangle();

            Size borderSize = DialogExtenders.GetBorderSize();
            rect.Offset(-rect.Left, -rect.Top);

            Size captionButtonSize = DialogExtenders.GetCaptionButtonSize();
            Rectangle buttonRect = new Rectangle(rect.Right - borderSize.Width - captionButtonSize.Width, rect.Top + borderSize.Height,
                                    captionButtonSize.Width, captionButtonSize.Height);

            foreach (CaptionButton button in _captionButtons)
            {
                button.Bounds = buttonRect;
                buttonRect.X -= captionButtonSize.Width;
            }
        }

        /// <summary>
        /// Called when a property of a CommandButton has changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        private void OnCommandButtonPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // if a button is hovered or pressed invalidate
            if (e.PropertyName == "Pressed" || e.PropertyName == "Hovered")
                NcPaint(true);
        }

        /// <summary>
        /// Gets the command button at the specified position.
        /// </summary>
        /// <param name="point">The position.</param>
        /// <returns>the CaptionButton instance or null if no button was found.</returns>
        private CaptionButton CommandButtonFromPoint(Point point)
        {
            foreach (CaptionButton button in _captionButtons)
                if (button.Bounds.Contains(point)) return button;
            return null;
        }

        /// <summary>
        /// Gets the command button with the specified HitTest.
        /// </summary>
        /// <param name="hitTest">The hitTest.</param>
        /// <returns>the CaptionButton instance or null if no button was found.</returns>
        private CaptionButton CommandButtonByHitTest(HitTest hitTest)
        {
            foreach (CaptionButton button in _captionButtons)
            {
                if (button.HitTest == hitTest)
                {
                    return button;
                }
            }
            return null;
        }

        /// <summary>
        /// Redraws the non client area..
        /// </summary>
        public void Invalidate()
        {
            NcPaint(true);
        }

        /// <summary>
        /// Redraws the non client area.
        /// </summary>
        /// <param name="invalidateBuffer">if set to <c>true</c> the buffer is invalidated.</param>
        /// <returns>true if the original painting should be suppressed otherwise false.</returns>
        private bool NcPaint(bool invalidateBuffer)
        {
            bool result = false;

            IntPtr hdc = (IntPtr)0;
            Graphics g = null;
            Region region = null;
            IntPtr hrgn = (IntPtr)0;

            try
            {

                // prepare image bounds
                Size borderSize = DialogExtenders.GetBorderSize();
                int captionHeight = DialogExtenders.GetCaptionHeight();

                RECT rectScreen = new RECT();
                NativeMethod.GetWindowRect(_parentHandler, ref rectScreen);

                Rectangle rectBounds = rectScreen.ToRectangle();
                rectBounds.Offset(-rectBounds.X, -rectBounds.Y);

                //// prepare clipping
                Rectangle rectClip = rectBounds;
                region = new Region(rectClip);
                rectClip.Inflate(-borderSize.Width, -borderSize.Height);
                rectClip.Y += captionHeight;
                rectClip.Height -= captionHeight;

                //// create graphics handle
                hdc = NativeMethod.GetDCEx(_parentHandler, (IntPtr)0,
                    (DCXFlags.DCX_CACHE | DCXFlags.DCX_CLIPSIBLINGS | DCXFlags.DCX_WINDOW));
                g = Graphics.FromHdc(hdc);

                //// Apply clipping
                region.Exclude(rectClip);
                hrgn = region.GetHrgn(g);
                NativeMethod.SelectClipRgn(hdc, hrgn);

                //// create new buffered graphics if needed
                if (_bufferGraphics == null || _currentCacheSize != rectBounds.Size)
                {
                    if (_bufferGraphics != null)
                        _bufferGraphics.Dispose();

                    _bufferGraphics = _bufferContext.Allocate(g, new Rectangle(0, 0,
                                rectBounds.Width, rectBounds.Height));
                    _currentCacheSize = rectBounds.Size;
                    invalidateBuffer = true;
                }

                if (invalidateBuffer)
                {
                    // Create painting meta data for form
                    SkinningFormPaintData paintData = new SkinningFormPaintData(_bufferGraphics.Graphics, rectBounds)
                    {
                        Borders = borderSize,
                        CaptionHeight = captionHeight,
                        Active = _formIsActive,
                        HasMenu = false,
                        IconSize = SystemInformation.SmallIconSize,
                        IsSmallCaption =
                            captionHeight ==
                            SystemInformation.ToolWindowCaptionHeight
                    };

                    // create painting meta data for caption buttons
                    if (_captionButtons.Count > 0)
                    {
                        paintData.CaptionButtons = new CaptionButtonPaintData[_captionButtons.Count];
                        for (int i = 0; i < _captionButtons.Count; i++)
                        {
                            CaptionButton button = _captionButtons[i];
                            CaptionButtonPaintData buttonData = new CaptionButtonPaintData(_bufferGraphics.Graphics, button.Bounds)
                            {
                                Pressed = button.Pressed,
                                Hovered = button.Hovered,
                                Enabled = button.Enabled,
                                HitTest = button.HitTest
                            };
                            paintData.CaptionButtons[i] = buttonData;
                        }
                    }

                    // paint
                    result = this._engine.CurrentSkin.OnNcPaint(_parentHandler, paintData);
                }

                //// render buffered graphics 
                if (_bufferGraphics != null)
                    _bufferGraphics.Render(g);
            }
            catch (Exception)
            {
                // error drawing
                result = false;
            }

            // cleanup data
            if (hdc != (IntPtr)0)
            {
                NativeMethod.SelectClipRgn(hdc, (IntPtr)0);
                NativeMethod.ReleaseDC(_parentHandler, hdc);
            }
            if (region != null && hrgn != (IntPtr)0)
                region.ReleaseHrgn(hrgn);

            if (region != null)
                region.Dispose();

            if (g != null)
                g.Dispose();

            return result;
        }

        /// <summary>
        /// Performs the non client HitTest
        /// </summary>
        /// <param name="m">The Message</param>
        /// <returns>true if the orginal handler should be suppressed otherwise false.</returns>
        private bool NcHitTest(ref Message m)
        {
            Point point = new Point(m.LParam.ToInt32());

            RECT rectScreen = new RECT();
            NativeMethod.GetWindowRect(_parentHandler, ref rectScreen);

            Rectangle rect = rectScreen;

            // custom processing
            if (rect.Contains(point))
            {
                Size borderSize = DialogExtenders.GetBorderSize();
                rect.Inflate(-borderSize.Width, -borderSize.Height);

                // let form handle hittest itself if we are on borders
                if (!rect.Contains(point))
                    return false;

                Rectangle rectCaption = rect;
                rectCaption.Height = DialogExtenders.GetCaptionHeight();

                // not in caption -> client
                if (!rectCaption.Contains(point))
                {
                    m.Result = (IntPtr)(int)HitTest.HTCLIENT;
                    return true;
                }

                // on Button?
                Point pt = new Point(point.X - rectScreen.X, point.Y - rectScreen.Y);
                CaptionButton sysButton = CommandButtonFromPoint(pt);
                if (sysButton != null)
                {
                    m.Result = (IntPtr)sysButton.HitTest;
                    return true;
                }

                // on Caption?
                m.Result = (IntPtr)(int)HitTest.HTCAPTION;
                return true;
            }
            m.Result = (IntPtr)(int)HitTest.HTNOWHERE;
            return true;
        }

        /// <summary>
        /// Handles the left button up message
        /// </summary>
        /// <param name="m">The message</param>
        /// <returns></returns>
        private bool OnNcLButtonUp(Message m)
        {
            // do we have a pressed button?
            if (_pressedButton != null)
            {
                // get button at wparam
                CaptionButton button = CommandButtonByHitTest((HitTest)m.WParam);
                if (button == null)
                    return false;

                if (button.Pressed)
                {
                    switch (button.HitTest)
                    {
                        case HitTest.HTCLOSE:
                            NativeMethod.SendMessage(this._parentHandler, 0x0010, new IntPtr(0), new IntPtr(0));
                            return true;
                        default:
                            break;
                    }
                }

                _pressedButton.Pressed = false;
                _pressedButton.Hovered = false;
                _pressedButton = null;
            }
            return false;
        }
        #endregion
    }
}
